var gamejs = require("gamejs");
var matchModule = require("js/match");
var obstacleModule = require("js/obstacle");
var turretModule = require("js/turret");
var radarModule = require("js/radar");
var enemyModule = require("js/enemy");

/**
 * Map : an object representing a map. It stores a matrix of the navigable terrain
 * and the lists (as gamejs.Group object) of enemies, radars, turrets and obstacles.
 */

/**
 * Creates a map with the given dimensions.
 * 
 * @param {Match} match The Match object where this object will work.
 * @param {number} width The map width, must be greater than zero.
 * @param {number} height The map height, must be greater than zero.
 * @throws {TypeError} If match isn't a Match object.
 * @throws {RangeError} If width or height aren't greater than zero.
 * 
 * @constructor
 */
var Map = exports.Map = function(match, width , height) {
	if( ! (match instanceof matchModule.Match)) {
		throw new TypeError("match isn't a valid Match object");
	}
	
	if(width <= 0 || height <= 0) {
		throw new RangeError("width and height must be greater than zero");
	}
	
	//creates the matrix of the navigable terrain
	//starts with all navigable terrain
	this.matrix = [];
	for (var i = 0; i < width; i++) {
		this.matrix[i] = [];
		for(var j = 0; j < height; j++) {
			this.matrix[i][j] = true;
		}
	}
	this.obstacles = new gamejs.sprite.Group();
	this.enemies = new gamejs.sprite.Group();
	this.turrets = new gamejs.sprite.Group();
	this.radars = new gamejs.sprite.Group();
	this.size = [width, height];
	this.currentMatch = match;
	
	this.background = gamejs.image.load(imgPath + "background.png");
};

/**
 * Returns the Match object where this map work.
 *
 * @return {Match} The match where this map work.
 */
Map.prototype.getMatch = function() {
    return this.currentMatch;
};

/**
 * Returns a Group object which contains the sprites of the enemies.
 *
 * @return {gamejs.sprite.Group} The enemies Group.
 */
Map.prototype.getEnemies = function() {
	return this.enemies ;
};

/**
 * Returns a Group object which contains the sprites of the obstacles.
 *
 * @return {gamejs.sprite.Group} The obstacles Group.
 */
Map.prototype.getObstacles = function() {
	return this.obstacles;
};

/**
 * Returns a Group object which contains the sprites of the turrets.
 *
 * @return {gamejs.sprite.Group} The turrets Group.
 */
Map.prototype.getTurrets = function() {
	return this.turrets;
};

/**
 * Returns a Group object which contains the sprites of the radars.
 *
 * @return {gamejs.sprite.Group} The radars Group.
 */
Map.prototype.getRadars = function() {
	return this.radars;
};

/**
 * Returns the map size.
 *
 * @returns {[number, number]}  The map size in [width, height] format.
 */
Map.prototype.getSize = function() {
	return this.size;
};

/**
 * Checks if a point is inside the map.
 * 
 * @param {[number, number]} The coordinates of the point to check, in [x, y] format.
 * @return {boolean} True if the given point is inside the map, false otherwise.
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 */
Map.prototype.isInside = function(coordinates) {
	if((coordinates.length != 2) ||
		(typeof coordinates[X] != "number") || (typeof coordinates[Y] != "number")) {
		throw new TypeError("Coordinates not in the correct form");
	}
	return (((coordinates[X] >= 0) && (coordinates[X] < this.size[WIDTH])) &&//x is in range
		((coordinates[Y] >= 0) && (coordinates[Y] < this.size[HEIGHT])));//y is in range
};

/**
 * Fills a pixel to make it not navigable.
 *
 * @param {[number, number]} coordinates The pixel coordinates as vector [x, y].
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.fill = function(coordinates) {
	if( ! this.isInside(coordinates)) {
		throw new RangeError("Invalid coordinates");
	}
	this.matrix[coordinates[X]][coordinates[Y]] = false;
};

/**
 * Clears a pixel to make it navigable.
 *
 * @param {[number, number]} coordinates The pixel coordinates as vector [x, y].
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.clear = function(coordinates) {
	if( ! this.isInside(coordinates)) {
		throw new RangeError("Invalid coordinates");
	}
	this.matrix[coordinates[X]][ coordinates[Y]] = true;
};

/**
 * Tells if the pixel is filled(not navigable).
 *
 * @param {[number, number]} coordinates The pixel coordinates as vector [x, y].
 * @returns {boolean} True if the pixel is filled(not navigable), false otherwise.
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.isFilled = function(coordinates) {
	if( ! this.isInside(coordinates)) {
		throw new RangeError("Invalid coordinates");
	}
	
	return  ! this.matrix[coordinates[X]][coordinates[Y]];
};

/**
 * Fills a circle making not navigable its area.
 * 
 * @param {[number, number]} center The center of the circle to use as vector [x, y].
 * @param {number} radius The radius of the circle to use, must be greater than zero.
 * @throws {TypeError} If center coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If center isn't inside the map or if radius isn't greater than zero.
 */
Map.prototype.fillCircle = function(center, radius) {
	this.fillCircleWith(center, radius, false);
};

/**
 * Clears a circle making navigable its area.
 * 
 * @param {[number, number]} center The center of the circle to use as vector [x, y].
 * @param {number} radius The radius of the circle to use, must be greater than zero.
 * @throws {TypeError} If center coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If center isn't inside the map or if radius isn't greater than zero.
 */
Map.prototype.clearCircle = function(center, radius) {
	this.fillCircleWith(center, radius, true);
};

/**
 * Fills a circle with the given value.
 * 
 * @param {[number, number]} center The center of the circle to use as vector [x, y].
 * @param {number} radius The radius of the circle to use, must be greater than zero.
 * @param {boolean} navigable Sets the value to give to the circle area:
 * false for fill(not navigable); true for clear(navigable).
 * @throws {TypeError} If center coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If center isn't inside the map or if radius isn't greater than zero.
 */
Map.prototype.fillCircleWith = function(center, radius, navigable) {
	if( ! this.isInside(center)) {
		throw new RangeError("Invalid center coordinates");
	}
	if( ! (radius > 0)) {
		throw new RangeError("radius must be greater than zero");
	}
	
	//work on the square circumscribed about the circle
	for(var x = center[X] - radius; x < center[X] + radius; x++) {
		for(var y = center[Y] - radius; y < center[Y] + radius; y++) {
			//calculates the distance from center with Pythagoras
			var dx = x - center[X];
			var dy = y - center[Y];
			var distance = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
			if((distance <= radius) && (this.isInside([x, y]))) {
				this.matrix[x][y] = navigable;
			}
		}
	}
};

/**
 * Adds an obstacle to the map and fills its area, making it not navigable.
 *
 * @param {Obstacle} obstacle The Obstacle object to add to the map.
 * @throws {TypeError} If obstacle isn't a Obstacle object.
 * @throws {Error(InvalidOperationError)} If obstacle hasn't the same Match object of this map.
 * @throws {Error(InvalidPositionError)} If the obstacle area is already occupied by 
 * a radar or a turret.
 */
Map.prototype.addObstacle = function(obstacle) {
	if( ! (obstacle instanceof obstacleModule.Obstacle)) {
		throw new TypeError("obstacle isn't a valid Obstacle object");
	}
	
	if(obstacle.getMatch() !== this.currentMatch) {
		throw new Error("InvalidOperationError : obstacle and this map haven't the same match");
	}
	
	//checks if the underlying area is not occupied by radars or turrets
	var collRadar = gamejs.sprite.spriteCollide(obstacle, this.radars, false, gamejs.sprite.collideCircle);
	var collTurret = gamejs.sprite.spriteCollide(obstacle, this.turrets, false, gamejs.sprite.collideCircle);
	if((collRadar.length + collTurret.length) > 0) {
		throw new Error("InvalidPositionError : this area is occupied");
	}
	
	this.obstacles.add(obstacle);//adds to the obstacles Group
	var center = obstacle.getCenter();
	var radius = obstacle.getRadius();
	this.fillCircle(center, radius);//fill the area
};

/**
 * Removes an obstacle from the map and clears its area, making it navigable.
 *
 * @param {Obstacle} obstacle The Obstacle object to remove from the map.
 * @throws {TypeError} If obstacle isn't a Obstacle object.
 * @throws {Error(InvalidOperationError)} If obstacle isn't present in the map or
 * if it's present but isn't saleable.
 */
Map.prototype.removeObstacle = function(obstacle) {
	if( ! (obstacle instanceof obstacleModule.Obstacle)) {
		throw new TypeError("obstacle isn't a valid Obstacle object");
	}
	
	if( ! this.obstacles.has(obstacle)) {
		throw new Error("InvalidOperationError : obstacle isn't present in this map");
	}
	
	if(obstacle.getSaleable()) {
		this.obstacles.remove(obstacle);//removes from the obstacles Group
		var center = obstacle.getCenter();
		var radius = obstacle.getRadius();
		this.clearCircle(center, radius);//clears the area
		
		var collObstacles = gamejs.sprite.spriteCollide(obstacle, this.obstacles, false, gamejs.sprite.collideCircle);
		if(collObstacles.length > 0) {
			//there are others obstacles in the same area
			//refill them
			collObstacles.forEach(function(obs) {
				var center = obs.getCenter();
				var radius = obs.getRadius();
				this.fillCircle(center, radius);//refill the area
			});
		}
		this.currentMatch.changeCredit(obstacle.getResaleValue());
	}
	else {
		throw new Error("InvalidOperationError : can't remove this obstacle");
	}
};

/**
 * Adds a radar to the map and fills its obstructive area, making it not navigable.
 *
 * @param {Radar} radar The Radar object to add to the map.
 * @throws {TypeError} If radar isn't a Radar object.
 * @throws {Error(InvalidOperationError)} If radar hasn't the same Match object of
 * this map or or the player hasn't enough credits to build this turret.
 * @throws {Error(InvalidPositionError)} If the radar area is already occupied by 
 * a radar, a turret or an obstacle.
 */
Map.prototype.addRadar = function(radar) {
	if( ! (radar instanceof radarModule.Radar)) {
		throw new TypeError("radar isn't a valid Radar object");
	}
	
	if(radar.getMatch() !== this.currentMatch) {
		throw new Error("InvalidOperationError : radar and this map haven't the same match");
	}
	
	//checks if the underlying area is not occupied by radars, turrets, obstacles
	var collRadar = gamejs.sprite.spriteCollide(radar, this.radars, false, gamejs.sprite.collideCircle);
	var collTurret = gamejs.sprite.spriteCollide(radar, this.turrets, false, gamejs.sprite.collideCircle);
	var collObstacle = gamejs.sprite.spriteCollide(radar, this.obstacles, false, gamejs.sprite.collideCircle);
	if((collRadar.length + collTurret.length + collObstacle.length) > 0) {
		throw new Error("InvalidPositionError : this area is occupied");
	}
	
	try {
		this.currentMatch.changeCredit( - radar.priceToBuild());
	}
	catch(e) {
		if(e.name == "RangeError") {
			throw new Error("InvalidOperationError : not enough credits");
		}
		else {
			throw e;
		}
	}
	
	this.radars.add(radar);//adds to the radars Group
	var center = radar.getCenter();
	var radius = radar.getObstructiveRadius();
	this.fillCircle(center, radius);//fills the obstructive area
};

/**
 * Removes a radar and clears its obstructive area, making it navigable.
 *
 * @param {Radar} radar The Radar object to remove from the map.
 * @throws {TypeError} If radar isn't a Radar object.
 * @throws {Error(InvalidOperationError)} If radar isn't present in the map or
 * if it's present but isn't saleable.
 */
Map.prototype.removeRadar = function(radar) {
	if( ! (radar instanceof radarModule.Radar)) {
		throw new TypeError("radar isn't a valid Radar object");
	}
	
	if( ! this.radars.has(radar)) {
		throw new Error("InvalidOperationError : radar isn't present in this map");
	}
	
	if(radar.getSaleable()) {
		this.radars.remove(radar);//removes from the radars Group
		var center = radar.getCenter();
		var radius = radar.getObstructiveRadius();
		this.clearCircle(center, radius);//clears the obstructive area
		this.currentMatch.changeCredit(radar.getResaleValue());
	}
	else {
		throw new Error("InvalidOperationError : can't remove this radar");
	}
};

/**
 * Tells if there are radars in the map.
 *
 * @return {boolean} True if there are radars in the map, false otherwise.
 */
Map.prototype.hasRadars = function() {
	return (this.radars.sprites().length > 0);
};

/**
 * Adds a turret to the map and fills its obstructive area, making it not navigable.
 *
 * @param {Turret} turret The Turret object to add to the map.
 * @throws {TypeError} If turret isn't a Turret object.
 * @throws {Error(InvalidOperationError)} If turret hasn't the same Match object of this map
 * or the player hasn't enough credits to build this turret.
 * @throws {Error(InvalidPositionError)} If the turret area is already occupied by 
 * a radar, a turret or an obstacle.
 */
Map.prototype.addTurret = function(turret) {
	if( ! (turret instanceof turretModule.Turret)) {
		throw new TypeError("turret isn't a valid Turret object");
	}
	
	if(turret.getMatch() !== this.currentMatch) {
		throw new Error("InvalidOperationError : turret and this map haven't the same match");
	}
	
	//checks if the underlying area is not occupied by radars, turrets, obstacles
	var collRadar = gamejs.sprite.spriteCollide(turret, this.radars, false, gamejs.sprite.collideCircle);
	var collTurret = gamejs.sprite.spriteCollide(turret, this.turrets, false, gamejs.sprite.collideCircle);
	var collObstacle = gamejs.sprite.spriteCollide(turret, this.obstacles, false, gamejs.sprite.collideCircle);
	if((collRadar.length + collTurret.length + collObstacle.length) > 0) {
		throw new Error("InvalidPositionError : this area is occupied");
	}
	
	try {
		this.currentMatch.changeCredit( - turret.priceToBuild());
	}
	catch(e) {
		if(e.name == "RangeError") {
			throw new Error("InvalidOperationError : not enough credits");
		}
		else {
			throw e;
		}
	}

	this.turrets.add(turret);//adds to the turret Group
	var center = turret.getCenter();
	var radius = turret.getObstructiveRadius();
	this.fillCircle(center, radius);//fills the obstructive area
};

/**
 * Removes a turret and clears its obstructive area, making it navigable.
 *
 * @param {Turret} turret The Turret object to remove from the map.
 * @throws {TypeError} If turret isn't a Turret object.
 * @throws {Error(InvalidOperationError)} If turret isn't present in the map or
 * if it's present but isn't saleable.
 */
Map.prototype.removeTurret = function(turret) {
	if( ! (turret instanceof turretModule.Turret)) {
		throw new TypeError("turret isn't a valid Turret object");
	}
	
	if( ! this.turrets.has(turret)) {
		throw new Error("InvalidOperationError : turret isn't present in this map");
	}
	
	if(turret.getSaleable()) {
		this.turrets.remove(turret);//removes from the turrets Group
		var center = turret.getCenter();
		var radius = turret.getObstructiveRadius();
		this.clearCircle(center, radius);//clears the obstructive area
		this.currentMatch.changeCredit(turret.getResaleValue());
	}
	else {
		throw new Error("InvalidOperationError : can't remove this turret");
	}
};

/**
 * Tells if there are turrets in the map.
 *
 * @return {boolean} True if there are turrets in the map, false otherwise.
 */
Map.prototype.hasTurrets = function() {
	return (this.turrets.sprites().length > 0);
};                  

/**
 * Adds an enemy to the map.
 *
 * @param {Enemy} enemy The Enemy object to add to the map.
 * @throws {TypeError} If enemy isn't a Enemy object.
 * @throws {Error(InvalidOperationError)} If enemy hasn't the same Match object of this map.
 */
Map.prototype.addEnemy = function(enemy) {
	if( ! (enemy instanceof enemyModule.Enemy)) {
		throw new TypeError("enemy isn't a valid Enemy object");
	}
	
	if(enemy.getMatch() !== this.currentMatch) {
		throw new Error("InvalidOperationError : enemy and this map haven't the same match");
	}
	
	this.enemies.add(enemy);//adds to the enemies Group
};

/**
 * Removes an enemy.
 *
 * @param {Enemy} enemy The Enemy object to remove from the map.
 * @throws {TypeError} If enemy isn't a Enemy object.
 * @throws {Error(InvalidOperationError)} If enemy isn't present in the map.
 */
Map.prototype.removeEnemy = function(enemy) {
	if( ! (enemy instanceof enemyModule.Enemy)) {
		throw new TypeError("enemy isn't a valid Enemy object");
	}
	
	if( ! this.enemies.has(enemy)) {
		throw new Error("InvalidOperationError : enemy isn't present in this map");
	}
	
	this.enemies.remove(enemy);//removes to the enemies Group
};

/**
 * Provides all the valid adjacent points of the given point.
 * 
 * @param {[numer, number]} coordinates The coordinates of the point to use as vector [x, y].
 * @return {Array} Array of coordinates of the the valid adjacent points as vector [x, y].
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.adjacent = function(coordinates) {
	if( ! this.isInside(coordinates)) {
		throw new RangeError("Invalid coordinates");
	}
	//takes all the possible directions
	var directions = [
		[0, -1], [-1, 0], [1, 0], [0, 1],
		[-1, -1], [1, -1], [1, 1], [-1, 1]
	];
	
	//takes adjacent points
	var allAdjacent = directions.map( function(dir) {
		return [(coordinates[X] + dir[X]), (coordinates[Y] + dir[Y])];
	});
	
	//removes if not in the map or if filled
	var validAdjacent = allAdjacent.filter( function(coordinates) {
		return ((this.isInside(coordinates)) &&//in map
			( ! this.isFilled(coordinates)));//not filled
	}, this);
	
	return validAdjacent;
};

/**
 * Provides all the valid points reachable from the given coordinates point with stepLength
 * single movements to north, north-east, east, south-east, south, south-west, west, north-west.
 * 
 * @param {[numer, number]} coordinates The coordinates of the point to use as vector [x, y].
 * @param {number} stepLength The number of single movements to do.
 * @return {Array} Array of coordinates of the the valid reachable points as vector [x, y].
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.reachable = function(coordinates, stepLength) {
	if( ! this.isInside(coordinates)) {
		throw new RangeError("Invalid coordinates");
	}
	//takes all the possible directions
	var directions = [
		[0, -stepLength], [-stepLength, 0], [stepLength, 0], [0, stepLength],
		[-stepLength, -stepLength], [stepLength, -stepLength], [stepLength, stepLength], [-stepLength, stepLength]
	];
	
	//takes reachable points
	var allReachable = directions.map( function(dir) {
		return [(coordinates[X] + dir[X]), (coordinates[Y] + dir[Y])];
	});
	
	//removes if not in the map or if filled
	var validReachable = allReachable.filter( function(coordinates) {
		return ((this.isInside(coordinates)) &&//in map
			( ! this.isFilled(coordinates)));//not filled
	}, this);
	
	return validReachable;
};

/**
 * Returns the actual distance from two adjacent points. 
 * 
 * @param {[numer, number]} startPoint Coordinates of the point from where to start, as vector [x, y].
 * @param {[numer, number]} endPoint Coordinates of the point to reach, as vector [x, y].
 * @return {number} The actual distance between the two points.
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.actualDistance = function(startPoint, endPoint) {
	if( ! this.isInside(startPoint) ||  ! this.isInside(endPoint)) {
		throw new RangeError("Invalid coordinates");
	}
	
	if((startPoint[X] != endPoint[X]) && (startPoint[Y] != endPoint[Y])) {
		return 1.41;
	}
	else {
		return 1;
	}
};

/**
 * Returns the estimated distance from two points, using the Manhattan heuristic.
 * 
 * @param {[numer, number]} startPoint Coordinates of the point from where to start, as vector [x, y].
 * @param {[numer, number]} endPoint Coordinates of the point to reach, as vector [x, y].
 * @return {number} The estimated distance between the two points.
 * @throws {TypeError} If coordinates isn't in the correct form [number, number].
 * @throws {RangeError} If coordinates isn't inside the map.
 */
Map.prototype.estimatedDistance = function(startPoint, endPoint) {
	if( ! this.isInside(startPoint) ||  ! this.isInside(endPoint)) {
		throw new RangeError("Invalid coordinates");
	}
	//using the Manhattan heuristic
	var dx = Math.abs(startPoint[X] - endPoint[X]);
	var dy = Math.abs(startPoint[Y] - endPoint[Y]);
	
	return (dx + dy);
};

/**
 * Updates the map calling update(msDuration) on all the Group; it follows
 * this order: updates the obstacles, updates the turrets, updates the radars,
 * updates the enemies.
 * 
 * @param {number} msDuration The time past from the last call, in ms.
 */
Map.prototype.update = function(msDuration) {
	//this.obstacles.update(msDuration);nothing to update here
	this.turrets.update(msDuration);
	this.radars.update(msDuration);
	this.enemies.update(msDuration);
};

/**
 * Draws the map in the given surface, following this order:
 * draws the background, draws the obstacles, draws the turrets,
 * draws the radars, draws the enemies.
 
 * @param {gamejs.Surface} surface The Surface object where to draw.
 */
Map.prototype.draw = function(surface) {
	surface.blit(this.background);
	//this.obstacles.draw(surface);
	this.obstacles.sprites().forEach(function (obs) {
		obs.draw(surface);
		gamejs.draw.circle(surface, "#C0C0C0", obs.getCenter(), obs.getRadius(), 1);
	});
	this.turrets.draw(surface);
	//this.radars.draw(surface);
	this.radars.sprites().forEach(function (rad) {
		rad.draw(surface);
		gamejs.draw.circle(surface, "#C0C0C0", rad.getCenter(), rad.getObstructiveRadius(), 1);
	});
	this.enemies.draw(surface);
};